//
//  CodingError.swift
//  MoonKit
//
//  Created by moon on 2019/3/7.
//  Copyright © 2019 moon. All rights reserved.
//

import Foundation

public enum CodingError : Error , CustomDebugStringConvertible , CustomStringConvertible{
    public struct Context : CustomDebugStringConvertible , CustomStringConvertible{
		/// The path of coding keys taken to get to the point of the failing encode
		/// call.
		public let codingPath: [CodingKey]
		
		/// A description of what went wrong, for debugging purposes.
		public let reason: String
        public var debugDescription: String {
            return self.description
        }
        public var description: String {
            let pathString = codingPath
                .map { (key) -> String in key.stringValue }
                .joined(separator: ".")
            var desc = "CodingPath is `\(pathString)`. Fail reason is `\(reason)`. "
            if let underlyingError = self.underlyingError {
                desc += "UnderlyingError is `\(String(describing: underlyingError))`."
            }
            return desc
        }
		/// The underlying error which caused this error, if any.
		public let underlyingError: Error?
		
		public init(codingPath: [CodingKey],debugDescription: String,underlyingError: Error? = nil) {
			self.codingPath = codingPath
			self.reason = debugDescription
			self.underlyingError = underlyingError
		}
        public init(preCodingPath: [CodingKey], current: Context) {
            self.codingPath = preCodingPath+current.codingPath
            self.reason = current.reason
            self.underlyingError = current.underlyingError
        }
	}
	
	case invalidValue(Any, CodingError.Context)
	
	/// 解码码时,类型符合
	case typeMismatch(Any.Type, CodingError.Context)
	
	/// 解码时,key对应的value不存在
	case valueNotFound(Any.Type, CodingError.Context)
	
	/// 解码时,key不存在
	case keyNotFound(CodingKey, CodingError.Context)
	
	/// 容器数据格式不对
	case dataCorrupted(CodingError.Context)
	
	/// 编解码unkeyed容器时,索引超出范围
	case indexOutOfRange(Int,CodingError.Context)
	
	case invalidOperation
	
	case convertFail(CodingError.Context)
    
    /// 无法decode数据到对象中。比如NSNumber,无法修改自身属性，只能重新赋值
    case canNotDecodeTo(CodingError.Context)
    
    public init(preCodingPath: [CodingKey], current: CodingError){
        switch current {
        case .invalidValue(let v, let context):
            self = .invalidValue(v,Context.init(preCodingPath: preCodingPath, current: context))
        case .typeMismatch(let v, let context):
            self = .typeMismatch(v,Context.init(preCodingPath: preCodingPath, current: context))
        case .valueNotFound(let v, let context):
            self = .valueNotFound(v,Context.init(preCodingPath: preCodingPath, current: context))
        case .keyNotFound(let v, let context):
            self = .keyNotFound(v,Context.init(preCodingPath: preCodingPath, current: context))
        case .dataCorrupted(let context):
            self = .dataCorrupted(Context.init(preCodingPath: preCodingPath, current: context))
        case .indexOutOfRange(let v, let context):
            self = .indexOutOfRange(v,Context.init(preCodingPath: preCodingPath, current: context))
        case .convertFail(let context):
            self = .convertFail(Context.init(preCodingPath: preCodingPath, current: context))
        case .invalidOperation:
            self = .invalidOperation
        case .canNotDecodeTo(let context):
            self = .canNotDecodeTo(Context.init(preCodingPath: preCodingPath, current: context))
        }
    }
    public var debugDescription: String {
        return self.description
    }
    public var description: String {
        var desc = "test";
        switch self {
        case .invalidValue(let v, let context):
            desc = "InvalidValueError:\(v),Context:\(context.description)"
        case .typeMismatch(let v, let context):
            desc = "TypeMismatchError:\(v),Context:\(context.description)"
        case .valueNotFound(let v, let context):
            desc = "ValueNotFoundError:\(v),Context:\(context.description)"
        case .keyNotFound(let v, let context):
            desc = "KeyNotFoundError:\(v),Context:\(context.description)"
        case .dataCorrupted(let context):
            desc = "DataCorruptedError,Context:\(context.description)"
        case .indexOutOfRange(let v, let context):
            desc = "IndexOutOfRangeError:\(v),Context:\(context.description)"
        case .convertFail(let context):
            desc = "ConvertFailError,Context:\(context.description)"
        case .invalidOperation:
            desc = "InvalidOperationError"
        case .canNotDecodeTo(let context):
            desc = "CanNotDecodeToError,Context:\(context.description)"
        }
        return desc
    }
    public func printError(){
        switch self {
        case .keyNotFound(_, _):
            break
        default:
            print("error:\(self)")
        }
    }
}

extension CodingError {
	internal static func _indexOutOfRange(index:Int,range:ClosedRange<Int>,at path: [CodingKey]) -> CodingError {
		let description = "index \(index) out of range \(range)"
		return .indexOutOfRange(index, CodingError.Context(codingPath: path, debugDescription: description))
	}
	internal static func _indexOutOfRange(index:Int,range:Range<Int>,at path: [CodingKey]) -> CodingError {
		let description = "index \(index) out of range \(range)"
		return .indexOutOfRange(index, CodingError.Context(codingPath: path, debugDescription: description))
	}
	internal static func _typeMismatch(at path: [CodingKey], expectation: Any.Type, reality: Any) -> CodingError {
		let description = "Expected to coding \(expectation) but found \(_typeDescription(of: reality)) of `\(reality)` instead."
		return .typeMismatch(expectation, Context(codingPath: path, debugDescription: description))
	}
	fileprivate static func _typeDescription(of value: Any) -> String {
		if value is NSNull {
			return "a null value"
		} else if value is NSNumber /* FIXME: If swift-corelibs-foundation isn't updated to use NSNumber, this check will be necessary: || value is Int || value is Double */ {
			return "a number"
		} else if value is String {
			return "a string/data"
		} else if value is [Any] {
			return "an array"
		} else if value is [String : Any] {
			return "a dictionary"
		} else {
			return "\(type(of: value))"
		}
	}
	internal static func _invalidFloatingPointValue<T : FloatingPoint>(_ value: T, at codingPath: [CodingKey]) -> CodingError {
		let valueDescription: String
		if value == T.infinity {
			valueDescription = "\(T.self).infinity"
		} else if value == -T.infinity {
			valueDescription = "-\(T.self).infinity"
		} else {
			valueDescription = "\(T.self).nan"
		}
		
		let debugDescription = "Unable to code \(valueDescription) directly in JSON."
		return .invalidValue(value, CodingError.Context(codingPath: codingPath, debugDescription: debugDescription))
	}
}
